// asynchronous progress/cancel support
define(
  ["require",
   "jquery",
   "bip.ver",
   "bip.config",
   "bip.cache",
   "bip.base"],
  function(require) {
    "use strict";

    var $ = require("jquery");
    var $bip = require("bip.base");
    var $ver = require("bip.ver");
    var $config = require("bip.config");
    var $cache = require("bip.cache");

    var heartbeatTimer;		   
    var timerStartTime;
    var lastCallTime;
    function isJqueryXHR(promise){
       return promise && $.isFunction(promise.abort);
    }

    function isAsyncPromise(promise){
       return promise && promise.$_asyncId
              && $.isFunction(promise.promise) && $.isFunction(promise.state);
    }

    function PendingAsyncCollection() {
      this._asyncId = 0;
    }

    PendingAsyncCollection.prototype.allocAsyncId = function(){
        this._asyncId += 1;
        return this._asyncId;
    }

    PendingAsyncCollection.prototype.add = function(promise, asyncId, cancelCtx){
       if (!promise || !$.isFunction(promise.promise) || !$.isFunction(promise.state)){
         $bip.log("error: not async promise object, cannot add");
         return;
       }
       if (typeof(this[asyncId]) != "undefined" && this[asyncId] != promise){
         $bip.log("error: bug or internal corrupt, $_asyncId(" + promise.$_asyncId + ") duplicated");
         return;
       }
       promise.$_asyncCancelCtx = cancelCtx;
       promise.$_asyncId = asyncId;
       this[promise.$_asyncId] = promise;
    }

    PendingAsyncCollection.prototype.complete = function(asyncId){
       var promise = this[asyncId];
       if (!promise){
         return;
       }
       if (promise.state() == "pending"){
         $bip.log("error: try to complete a pending async operation");
         return;
       }
       delete this[promise.$_asyncId];
    }

    PendingAsyncCollection.prototype.cancel = function(promise){
       if (!isAsyncPromise(promise)){
         $bip.log("error: not async promise object, cannot cancel");
         return "cancel_success";
       }
       if (this[promise.$_asyncId] && this[promise.$_asyncId] != promise){
         $bip.log("error: bug or internal corrupt, $_asyncId is inconsistent with its promise object!");
         return "cancel_success";
       }
       if (promise.state() != "pending"){
         this.complete(promise.$_asyncId);
         return "async_complete";
       }

       $bip.log("cancel async op: " + promise.$_asyncId);
       promise.$_asyncCancelCtx.canceled = true;
       delete this[promise.$_asyncId];

       if (isJqueryXHR(promise)) {
         var jqXHR = promise;
         jqXHR.abort();
       }
       return "cancel_success";
    }

    PendingAsyncCollection.prototype.cancelAll = function(){
        var promises = [];
        for(id in this){
          if (id == "_asyncId") continue;
          promises.push(id);
        }
        for(id in promises){
          $bip.log("cancel async op: " + id);
          var promise = this[id];
          delete this[id];
          promise.$_asyncCancelCtx.canceled = true;
          if (isJqueryXHR(promise)) {
            var jqXHR = promise;
            jqXHR.abort();
          }
        }
    }

    var _pendingAsyncs = new PendingAsyncCollection();
    function allocAsyncId(){
      return _pendingAsyncs.allocAsyncId();
    }
    function addAsync(promise, asyncId, cancelCtx){
      return _pendingAsyncs.add(promise, asyncId, cancelCtx);
    }
    function completeAsync(promise){
      return _pendingAsyncs.complete(promise);
    }
    function cancelAsync(promise){
      return _pendingAsyncs.cancel(promise);
    }

    function getErrorMsg(url, jqXHR, textStatus, errorThrown) {
       var msg = "请求服务失败，URL: " + url + "\r\n";
       msg += "HTTP status: " + jqXHR.status + " " + jqXHR.statusText + ", status: " + textStatus + "\r\n";
       msg += "error: " + errorThrown + "\r\n";
       msg += "headers:\r\n" + jqXHR.getAllResponseHeaders();
       msg += "body:\r\n" + jqXHR.responseText;
       return msg;
    }

    var asyncCallDefaultOpts = {
       type : "post",
       dataType : "json",
       global: false,
       headers: {},
       async_error : "showmsg",
       handleBipError : true,
       async_notifyDelaySeconds : 2,
       // async_notify : user callback function(data)
    };

    function isBipError(data){
       return $.isPlainObject(data) && data.error;
    }

    function asyncCall(url, options, thisArg){
       var $deferred = $.Deferred();
       var opts = $.extend({}, asyncCallDefaultOpts, options);
       $.extend(opts.headers, { "X-BIP-JSVersion": $ver.buildVersion });

       var _timerId = null;
       var _asyncId = allocAsyncId();
       var _cancelCtx = {};
       var $promise = $.ajax(url, opts);

       $promise.fail(function(jqXHR, textStatus, errorThrown){
         window.clearTimeout(_timerId);
         _timerId = null;
         completeAsync(_asyncId);
         if (_cancelCtx.canceled){
           $bip.log("onerror, async op canceled: " + _asyncId);
           textStatus = "canceled";
         }
         else{
           if (opts.async_error == "showmsg"){
              $bip.showMsg(getErrorMsg(url, jqXHR, textStatus, errorThrown));
           }
           else if (opts.async_error == "logging"){
              $bip.log(getErrorMsg(url, jqXHR, textStatus, errorThrown));
           }
         }
         $deferred.rejectWith(thisArg, [errorThrown, textStatus, jqXHR]);
       })
       .done(function(data, textStatus, jqXHR){
          window.clearTimeout(_timerId);
          _timerId = null;
          completeAsync(_asyncId);
          if (_cancelCtx.canceled) {
             $bip.log("onsuccess, async op canceled: " + _asyncId);
             $deferred.resolveWith(thisArg, [data, textStatus, jqXHR]);
             return;
          }

          if (isBipError(data)) {
             if(data.error.name && data.error.name == "RequestUnsafe"){
               window.location = "unsafe.html"; // destroy whole web app
               return;
             }
             if(opts.handleBipError){
               // todo, refine the error message.
               $bip.showMsg("错误", "请求服务或获取内容发生错误\r\nurl: " + url
                         + "\r\nerror:\r\n" + JSON.stringify(data.error));
             }
             $deferred.rejectWith(thisArg, [data, textStatus, jqXHR]);
          }
          else {
             $deferred.resolveWith(thisArg, [data, textStatus, jqXHR]);
          }
       });

       if ($promise.state() == "pending") {
         addAsync($promise, _asyncId, _cancelCtx);
         if ($.isFunction(opts.async_notify)){
            _timerId = setTimeout(function(){
                                    _timerId = null;
                                    opts.async_notify.apply(thisArg, [$promise]);
                                  },
                                  opts.async_notifyDelaySeconds * 1000
                                 );
         }
       }
       
       lastCallTime = $.now();
       var $res = $deferred.promise();
       $res.cancelablePromise = $promise;
       return $res;
    }

    var asyncGetDefaultOpts = {
       type : "get",
       dataType : "json",
       global: false,
       headers : {},
       async_error : "showmsg",   // function(jqXHR, textStatus, errorThrown)
       handleBipError : true,
       cacheEvenBipError : false,
       async_notifyDelaySeconds : 2,
       doneIfCacheHit : true,
       getETag : function(cacheHitVal) { return cacheHitVal.$_ETag; },
       setETag : function(data, etag) { data.$_ETag = etag; return data; },
       getLastModified : function(cacheHitVal) { return cacheHitVal.$_LastModified; },
       setLastModified : function(data, lastModified) { data.$_LastModified = lastModified; return data; },
       preUpdateCache : function(data) { return (!data.error && !data.Error); },
       //cache : one of $cache.disk|lru
       //cacheKey : cache key
    };

    function asyncGet(url, options, thisArg) {
       var opts = $.extend({}, asyncGetDefaultOpts, options);
       var etag = null;
       var lastModified = null;
       var cachedVal = null;
       var cache = opts.cache;
       var $cacheOn = $config.async_cache_enable;

       if ($cacheOn && cache) {
         var $deferred = $.Deferred();
         if (!opts.cacheKey) {
           opts.cacheKey = url;
         }
         cachedVal = cache.get(opts.cacheKey);
         if (typeof(cachedVal) != "undefined") {
           etag = opts.getETag(cachedVal);
           lastModified = opts.getLastModified(cachedVal);
           $bip.log("asyncGet cache hit. cacheKey: " + opts.cacheKey + ", url: " + url);
           $deferred.resolveWith(thisArg, [cachedVal, "cached"]);
           if (opts.doneIfCacheHit) {
             return $deferred.promise();
           }
         }
       }

       if (etag) {
         $.extend( opts.headers, {"If-None-Match" : etag,} );
       }
       if (lastModified) {
         $.extend( opts.headers, {"If-Modified-Since" : lastModified,} );
       }

       function shouldCache(data) {
          return cache && (opts.cacheEvenBipError || !isBipError(data));
       }

       var $promise = asyncCall(url, opts, thisArg);
       $promise.done(function(data, textStatus, jqXHR){
          if (jqXHR.status == "304" && cachedVal){ // not modified.
             return;
          }

          if (shouldCache(data)) {
            var newEtag = jqXHR.getResponseHeader("ETag");
            if (newEtag) {
              opts.setETag(data, newEtag);
            }
            var newLastModified = jqXHR.getResponseHeader("Last-Modified");
            if (newLastModified) {
              opts.setLastModified(data, newLastModified);
            }
            if (!$.isFunction(opts.preUpdateCache) || opts.preUpdateCache(data)) {
              cache.set(opts.cacheKey, data);
            }
          }
       });
       return $promise;
    }

    var asyncGetTmplDefaultOpts = {
       type : "get",
       dataType : "html",
       async_error : "showmsg",
       async_notifyDelaySeconds : 2,
       cache : $cache.disk,
    };
    
    function asyncGetTmpl(tmplName, options, thisArg) {
       var opts = $.extend({}, asyncGetTmplDefaultOpts, options);
       var $cacheOn = $config.async_cache_enable;

       var $deferred = $.Deferred();
       var tmplObj;// = $.templates[tmplName];
       if ($cacheOn && tmplObj) {
         $deferred.resolveWith(thisArg, [tmplObj, "cached"]);
         return $deferred.promise();
       }

       var url = "template/" + tmplName + ".tmpl";
       var $promise = asyncGet(url, opts, thisArg);
       $promise.done(function(data, textStatus, jqXHR){
         tmplObj = $.templates(tmplName, data);
         $deferred.resolveWith(thisArg, [tmplObj, textStatus, jqXHR]);
       })
       .fail(function(error, textStatus, jqXHR){
         $deferred.rejectWith(thisArg, [error, textStatus, jqXHR]);
       });

       var $res = $deferred.promise();
       $res.cancelablePromise = $promise.cancelablePromise;
       return $res;
    }

    function startHeartbeat(){       
        var $interval = $config.heartbeat_interval_seconds*1000;
        var $timeout = $config.idle_timeout_hours*3600*1000;

        if(heartbeatTimer){
        	 $bip.log("heartbeatTimer has started");
        	 return;
        }        

        timerStartTime = $.now();
        heartbeatTimer = setInterval(function(){
			var currentTime = $.now();
			var span;
			if(!lastCallTime){
				span = currentTime - timerStartTime;
			}else{
				span = currentTime - lastCallTime;
			}
			if(span > $timeout){
				window.location = "login?logout=1"; //exit				
			}
			if(span > $interval){
				if(lastCallTime == null) {
					return;
				}

		    	$.ajax("jsapi/general/keepSession", {
		            cache: false,
		            dataType: "json",
		            global: false,
		            error: function(jqXHR, textStatus, errorThrown){
		               $bip.log("keepSession failed, error status:", textStatus, " errorThrown:", errorThrown);
		            },
		            success: function(){}
		          }
		        );
		    	
			}
			
		}, $interval);
	
    }
    
    function onDocReady(){  
    	startHeartbeat();
    }

    return {
      cancelAsync : cancelAsync,
      asyncCall : asyncCall,
      asyncGet : asyncGet,
      asyncGetTmpl : asyncGetTmpl,
      onDocReady : onDocReady,
    };
  }
);

